{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 画图app"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "在第一章做时钟app时，我们用了Kivy的标准部件：布局，文本框和按钮。通过这些高层次的抽象，我们能够灵活的修改部件的外观—，可以使用一整套成熟的组件，而不仅仅是单个原始图形。这种方式并非放之四海而皆准，马上你就会看到，Kivy还提供了低层的抽象工具：画点和线。\n",
    "\n",
    "<!-- TEASER_END-->"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "我认为做画图app是自由绘画最好的方式。我们的应用会看着有点像Windows自带的画图程序。\n",
    "\n",
    "不同的是，我们的画图app支持多平台，包括Andorid和iOS。我们也忽略了图像处理的功能，像矩形选框，图层，保存文件等。这些功能可以自己练习。\n",
    "\n",
    ">关于移动设备：Kivy完全支持iOS开发，即使你没有类似开发经验也不难。因此，建议你先在熟悉的平台上快速实现app，这样就可以省略编译的时间和一堆细节。Android开发更简单，由于[Kivy Launcher](https://\n",
    "play.google.com/store/apps/details?id=org.\n",
    "kivy.pygame)可以让Kivy代码直接在Android上运行。\n",
    ">Kivy可以不用编译直接在Andorid上运行测试，相当给力，绝对RAD（rapid application development）。\n",
    ">窗口改变大小的问题，并没有广泛用于移动设备，Kivy应用在不同的移动设备和桌面系统平台使用类似的处理方式。因此，开始编写和调试都非常容易，直到版本确定的最后阶段才需要集中精力弥补这些问题。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "我们还会学习Kivy中两个相反的功能：触摸屏的多点触控和桌面系统的鼠标点击。\n",
    "\n",
    "作为移动设备的第一大法，Kivy为多点触控输入提供了一个模拟层，可以使用鼠标。可以通过右键激活功能。但是，这个多点触控模拟器并不适合真实的场景，仅适合调试用。\n",
    "\n",
    "画图app最会这这样：\n",
    "\n",
    "![paintapp](kbpic/2.1paintapp.png)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "##设置画板"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "我们的app通过root部件自动覆盖全局，整个屏幕都可以画画。到后面增加工具按钮的时候再调整。\n",
    "\n",
    "root部件是处于最外层，每个Kivy的app都有一个，可以根据app的需求制定任何部件作为root部件。比如上一章的时钟app，`BoxLayout`就是root部件；如果没其他要求，布局部件就是用来包裹其他控件的。\n",
    "\n",
    "现在这个画图app，我们需要root部件具有更多的功能；用户应该可以画线条，支持多点触控。不过Kivy没有自带这些功能，所以我们自己建。\n",
    "\n",
    "建立新部件很简单，只要继承Kivy的`Widget`类就行。如下所示："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "from kivy.app import App\n",
    "from kivy.uix.widget import Widget\n",
    "\n",
    "class CanvasWidget(Widget):\n",
    "   pass\n",
    "\n",
    "class PaintApp(App):\n",
    "   def build(self):\n",
    "       return CanvasWidget()\n",
    "\n",
    "if __name__ == '__main__':\n",
    "   PaintApp().run()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "这就是画图app的`main.py`，`PaintApp`类就是应用的起点。以后我们不会重复这些代码，只把重要的部分显示出来。\n",
    ">`Widget`类通常作为基类，就行Python的`object`和Java的`Object`。当它按照`as is`方式使用时，`Widget`功能极少。它没有可以直接拿来用的可视化的外观和属性。`Widget`的子类都是很简单易用的。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "##制作好看的外观"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "首先，让我们做个好看的外观，虽然不是核心功能，但长相影响第一印象。下面我们改改外观，包括窗口大小，鼠标形状。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "###可视化外观"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "我认为任何画图软件的背景色都应该是白的。和第一章类似，我们在`__name = '__main__'`后面加上就行："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "from kivy.core.window import Window\n",
    "from kivy.utils import get_color_from_hex\n",
    "\n",
    "Window.clearcolor = get_color_from_hex('#FFFFFF')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "你可能想把`import`语句放到前面，其实Kivy的一些模块导入有顺序要求，且会产生副作用，尤其是`Window`对象。这在好的Python程序中很少见，导入模块产生的副作用有点小问题。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "###窗口大小"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "另一个要改的就是窗口大小，下面的改变不影响移动设备。在桌面系统上，Kivy的窗口时可以调整的，后面我们会设置禁止调整。\n",
    "\n",
    ">如果目标设备明确，设置窗口大小是很有用的，这样就可以决定屏幕分辨率的参数，实现最好的适配效果。\n",
    "\n",
    "要改变窗口大小，就把下面的代码放到`from kivy.core.window import Window`上面。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "from kivy.config import Config\n",
    "\n",
    "Config.set('graphics', 'width', '960')\n",
    "Config.set('graphics', 'height', '540')  # 16:9"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "如果要禁止窗口调整："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "Config.set('graphics', 'resizable', '0')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "如果没有充分理由，千万别这么做，因为把窗口调整这点小自由从用户手中拿走实在太伤感情了。如果把应用像素精确到1px，移动设备用户可能就不爽了，而Kivy布局可以建立自适应的界面。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "###鼠标样式"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "之后就是改变鼠标光标的样式。Kivy没有支持，不过可以过Pygame实现，基于SDL窗口和OpenGL内容管理模块，在Kivy的桌面平台应用开发中用途广泛。如果你这么用，移动应用大都不支持Pygame。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "之后就是改变鼠标光标的样式。Kivy没有支持，不过可以过Pygame实现，基于SDL窗口和OpenGL内容管理模块，在Kivy的桌面平台应用开发中用途广泛。如果你这么用，移动应用大都不支持Pygame。\n",
    "![mousepointer](kbpic/2.2mousepointer.png)\n",
    "\n",
    "图中`@`是黑的，`-`是白的，其他字符是透明的。所以的线都是等宽的，且是8的倍数（SDL的限制）。鼠标的光标运行后是这样：\n",
    "![crosshair](kbpic/2.3crosshair.png)\n",
    "\n",
    ">当前的Pygame版本有个bug，`pygame.cursors.compile()`黑白显示颠倒。以后应该会修复。不过`pygame_compile_cursor()`是正确的方法，[Pygame的Simple DirectMedia Layer (SDL)兼容库](http://goo.gl/2KaepD)。\n",
    "\n",
    "现在，我们把光标应用到app中，替换`PaintApp.build`方法："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "from kivy.base import EventLoop\n",
    "class PaintApp(App):\n",
    "    def build(self):\n",
    "        EventLoop.ensure_window()\n",
    "        if EventLoop.window.__class__.__name__.endswith('Pygame'):\n",
    "            try:\n",
    "                from pygame import mouse\n",
    "                # pygame_compile_cursor is a fixed version of\n",
    "                # pygame.cursors.compile\n",
    "                a, b = pygame_compile_cursor()\n",
    "                mouse.set_cursor((24, 24), (9, 9), a, b)\n",
    "            except:\n",
    "                pass\n",
    "        return CanvasWidget()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "代码很简单，注意下面四点：\n",
    "- `EventLoop.ensure_window()`: 这个函数到app窗口 ( `EventLoop.window` ) 准备好才执行。\n",
    "- `EventLoop.window.__class__.__name__.endswith('Pygame')`:\n",
    "这个条件检查窗口名称Pygame，只是Pygame条件下才执行自定义光标。\n",
    "- `try ... except`模块里面是Pygame的`mouse.set_cursor`。\n",
    "- 变量`a`和`b`通过SDL构建了光标，表示异或(XOR)和与(AND)，都是SDL独有的实现方式。\n",
    ">[Pygame文档](http://www.pygame.org)提供了全部的api说明。\n",
    "\n",
    "现在做的这些比Kivy的模块更底层，并不常用，不过也不用害怕触及更多的细节。有很多功能只能通过底层的模块实现，因为Kivy还没达到面面俱到的程度。尤其是那些不能跨平台的功能，会涉及很多系统层的实现。\n",
    "\n",
    "Kivy/Pygame/SDL/OS的关系如下图所示：\n",
    "![multiapi](kbpic/2.4multiapi.png)\n",
    "\n",
    "SDL已经把系统底层的API都封装好了，兼容多个系统，Pygame再将SDL转换成Python，Kivy可以导入Pygame模块调用这些功能。\n",
    "\n",
    ">为什么不直接用SDL呢？可以看[SDL文档](https://www.libsdl.org/)。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "###多点触控模拟器"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "让运行桌面应用时，Kivy提供了一个模拟器实现多点触控操作。实际上是一个右击行为，获取半透明的点；按住右键时可以拖拽。\n",
    "\n",
    "如果你没有真实的多点触控设备，这个功能可能适合调试。但是，也会占用右键的功能。不调试的时候还是建议你禁用这个功能，避免对用户造成困扰。设置方法如下："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "Config.set('input', 'mouse', 'mouse,disable_multitouch')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "##触摸绘画"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "要实现用户通过触摸绘画的效果，可以在用户输入后屏幕会出现一个圆圈。\n",
    "\n",
    "部件如果带`on_touch_down`事件，就可以实现上述功能。正在需要的是点击位置的坐标，为`CanvasWidget`添加一个方法获取即可："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "class CanvasWidget(Widget):\n",
    "    def on_touch_down(self, touch):\n",
    "        print(touch.x, touch.y)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "要在屏幕上画画，我们就要实现`Widget.canvas`属性。Kivy的`canvas`属性是一个底层为OpenGL的可绘制层，不过没有底层图形API那么复杂，`canvas`可以持续保留我们画过的图。\n",
    "\n",
    "基本图形如圆（Color），线（Line）, 矩形（Rectangle），贝塞尔曲线（Bezier），可以通过`kivy.graphics`导入。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "###canvas简介"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "`Canvas`的API可以直接调用，也可以通过上下文关联`with`关键字调用。如下所示："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "self.canvas.add(Line(circle=(touch.x, touch.y, 25)))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "这里的`Line`元素的参数是图形命令队列。\n",
    "\n",
    ">如果你想立刻试验代码，请先看下一节**屏幕显示触摸轨迹**中更完整的例子。\n",
    "\n",
    "通过上下文关联with关键字调用可以让代码更简练，尤其是在同时操作多个指令时。下面的代码与之前一致："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "with self.canvas:\n",
    "    Line(circle=(touch.x, touch.y, 25))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "需要注意的是，如前面所说，canvas上后面调用的指令不会覆盖前面调用的指令；因此，canvas是一个不断增长的数组，里面都是不断显示元素的指令，更新频率60fps，但是也不能让canvas无限增长下去。\n",
    "\n",
    "例如，所见即所得的程序（如HTML5的`<canvas>`）里有一条设计规则就是通过背景色填充擦除之前的图像。在浏览器里面可以很直观的写出：\n",
    "\n",
    "```JavaScript\n",
    "// JavaScript code for clearing the canvas\n",
    "canvas.rect(0, 0, width, height)\n",
    "canvas.fillStyle = '#FFFFFF'\n",
    "canvas.fill()\n",
    "```\n",
    "\n",
    "在Kivy设计中，这种模型也是增加指令；首先获取前面所有的图形元素，然后把它们画成矩形。这个看着挺好其实不对："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "# 看着和avaScript代码一样，但是错了。\n",
    "with self.canvas:\n",
    "    Color(1, 1, 1)\n",
    "    Rectangle(pos=self.pos, size=self.size)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    ">和内存泄露差不多，这个bug很久没被发现，使代码冗余，性能降低。由于显卡加速的功能，包括智能手机运行速度都很快。所以很难意识到这是一个bug。为了清除Kivy的canvas，应该用`canvas.clear()`来清除所有指令，后面会介绍。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "###屏幕显示触摸轨迹"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "我们马上做一个按钮来清屏；现在让我们把触摸的轨迹显示出来。让我们把`print()`删掉，然后增加一个方法在`CanvasWidget`下面："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "class CanvasWidget(Widget):\n",
    "    def on_touch_down(self, touch):\n",
    "        with self.canvas:\n",
    "            Color(*get_color_from_hex('#0080FF80'))\n",
    "            Line(circle=(touch.x, touch.y, 25), width=4)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "这样就每次都会画一个空心圆在画布上。`Color`指令为`Line`取色。\n",
    "> 注意`hex('#0080FF80')`并不是CSS颜色格式，因为它有四个组成部分，表示alpha值，即透明度。类似于`rgb()`与`rgba()`的区别。\n",
    "\n",
    "可能你会觉得奇怪，我们用`Line`画的是圈，而不是直线。Kivy的图形元素具体很强的自定义功能，比如我们可以用`Rectangle`和`Triangle`画自定义的图片，用`source`参数设置即可。\n",
    "\n",
    "前面的程序效果如下图所示：\n",
    "![Displayingtouches](kbpic/2.5Displayingtouches.png)\n",
    "画图app完整的代码如下："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "# In main.py\n",
    "from kivy.app import App\n",
    "from kivy.config import Config\n",
    "from kivy.graphics import Color, Line\n",
    "from kivy.uix.widget import Widget\n",
    "from kivy.utils import get_color_from_hex\n",
    "\n",
    "class CanvasWidget(Widget):\n",
    "    def on_touch_down(self, touch):\n",
    "        with self.canvas:\n",
    "            Color(*get_color_from_hex('#0080FF80'))\n",
    "            Line(circle=(touch.x, touch.y, 25), width=4)\n",
    "\n",
    "class PaintApp(App):\n",
    "    def build(self):\n",
    "        return CanvasWidget()\n",
    "\n",
    "if __name__ == '__main__':\n",
    "    Config.set('graphics', 'width', '400')\n",
    "    Config.set('graphics', 'height', '400')\n",
    "    Config.set('input', 'mouse', 'mouse,disable_multitouch')\n",
    "    \n",
    "    from kivy.core.window import Window\n",
    "    Window.clearcolor = get_color_from_hex('#FFFFFF')\n",
    "    PaintApp().run()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "这里没有加入鼠标光标显示的部分。`paint.kv`文件也没有了，用`build()`方法返回根部件。\n",
    "\n",
    "注意`from kivy.core.window import Window`行，是由于有些模块有副作用，所有放在后面导入。`Config.set()`应该放在任何有副作用模块的前面。\n",
    "\n",
    "下面，我们增加一些特性，让画图app实现我们想要的功能。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "##清屏"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "到目前为止，我们清屏的做法就是重启程序。下面我们增加一个按钮来清屏。我们用上一章时钟app的按钮即可，没什么新鲜，有意思的是位置。\n",
    "\n",
    "上一章时钟app里面，我们没有讨论过位置，所有部件都放在`BoxLayouts`里面。现在我们的app没有任何布局，因为根部件就是`CanvasWidget`，我们没有实现任何子部件的位置。\n",
    "\n",
    "在Kivy里面，布局部件缺失表示每一个部件都可以随意设置位置和大小（类似的UI设计工具，如Delphi，Visual Basic等等都如此）。\n",
    "\n",
    "要让清屏按钮放在右上角，我们这么做：\n",
    "\n",
    "```yaml\n",
    "# In paint.kv\n",
    "<CanvasWidget>:\n",
    "    Button:\n",
    "        text: 'Delete'\n",
    "        right: root.right\n",
    "        top: root.top\n",
    "        width: 80\n",
    "        height: 40\n",
    "```\n",
    "\n",
    "按钮的`right`和`top`属性与根部件的属性一致。我们还可以进行数学运行，如`root.top – 20`。结果很直接，`right`和`top`属性都是绝对值。\n",
    "\n",
    "注意我们定义了一个`<CanvasWidget>`类却没有指定父类。这么做可以是因为我们在Python代码理论已经定义了一个同样的类。Kivy允许我们扩展所有的类，包括内部类，如`<Button>`和`<Label>`，以及自定义类。\n",
    "\n",
    "这里体现了Kivy语言描述对象的可视化属性的一个好思路，类似于MVC设计方法，让内容与逻辑分离。同时，也更好的保持了所有Python程序的结构不变。这种Python代码与Kivy语言分离的思想让程序更容易维护。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "###传递事件"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "如果你跟着教程看到现在，准备去按清屏键。你会发现没反应，因为还没有增加事件，所有没有反馈。所有单击按钮不会有动作，相反会在画布上留下空心圈。\n",
    "\n",
    "因为所有的触摸都是发生在`CanvasWidget.on_touch_down`上，并没有传递给其他子部件，所以清屏按钮没反应。不像HTML的DOMDOM，Kivy事件不会从嵌套的元素升级为父元素显示出来。它们走另一条路，如果事件传递到父元素没有反应，才从父元素下降到子元素。\n",
    "\n",
    "最直接的方式就是这样："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "# 注意：不是最优代码\n",
    "def on_touch_down(self, touch):\n",
    "    for widget in self.children:\n",
    "        widget.on_touch_down(touch)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "实际上，`Widget.on_touch_down`的默认行为有很多，所有我们可以直接调用，让代码更简练。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "def on_touch_down(self, touch):\n",
    "    if Widget.on_touch_down(self, touch):\n",
    "        return"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "如果事件被正常处理了，`on_touch_down`这个handler返回`True`。触摸按钮会返回`True`是因为按钮响应了，然后很快的改变其外观。这就是为了取消我们的事件处理需要做的事情，当我们画圈的时候，方法的第二个行就`return`。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "###清屏"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "现在我们回到清屏按钮上。其实很简单，就是下面两行："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "def clear_canvas(self):\n",
    "    self.canvas.clear()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "别忘了把事件绑定到`paint.kv`文件：\n",
    "\n",
    "```yaml\n",
    "Button:\n",
    "on_release: root.clear_canvas()\n",
    "```\n",
    "\n",
    "这样就可以清屏了，同时还把按钮也清除了。因为`CanvasWidget`是根部件，按钮是子部件。按钮部件本身没有被删除，它的画布`Button.canvas`从`CanvasWidget.canvas.children`层级中移除了，因此不存在了。\n",
    "\n",
    "要保留按钮，可以这样："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "def clear_canvas(self):\n",
    "    self.canvas.clear()\n",
    "    self.canvas.children = [widget.canvas \n",
    "                            for widget in self.children]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "但是这么做不够好，因为不同的部件初始化和运行方式不同。更好的做法是：\n",
    "\n",
    "1. 从`CanvasWidget`部件中删除所有子部件；\n",
    "2. 然后清除画布；\n",
    "3. 最后再重新增加子部件，这样它们就可以正确的初始化了。\n",
    "\n",
    "这个版本有点长，但是更合理："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "class CanvasWidget(Widget):\n",
    "    def clear_canvas(self):\n",
    "        saved = self.children[:]\n",
    "        self.clear_widgets()\n",
    "        self.canvas.clear()\n",
    "        for widget in saved:\n",
    "            self.add_widget(widget)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "解释一下`saved = self.children[:]`语句。`[:]`操作符是复制数组（就是“创建一个元素相同的数组”）。如果我们写`saved = self.children`，那就会从` self.children `和`saved`同时删除所有子部件。因为Python赋值是引用，与Kivy无关。\n",
    "> 如果想进一步了解Python的特性，可以看看[StackOverflow](http://stackoverflow.com/\n",
    "questions/509211)\n",
    "\n",
    "现在，我们已经可以用蓝色的圈钱画图了，如下所示。这当然并非最终版，请看下面的内容。\n",
    "![Deletebutton](kbpic/2.6Deletebutton.png)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "##连点成线"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "我们的app已经可以清屏了，不过只能画圈。下面在改进一下。\n",
    "\n",
    "要保持连续触控画线（按住然后拖拽），我们需增加一个监听器，`on_touch_move`。每次使用都会收到最新点的位置。\n",
    "\n",
    "如果我们一次只有一条线，我们可以把这条线保存为`self.current_line`。但是，由于这是多点触控，我们就要用其他方法来保存`touch`变量了。\n",
    "\n",
    "之所以能实现这些，是因为每个触控自始至终都访问相同的`touch`对象。还有一个`touch.ud`属性，是一个字典类型，`ud`就是用户数据(user data)，可以灵活的跟踪所有的触控。初始值为空字典`{}`。\n",
    "\n",
    "下面我们要做的是：\n",
    "- 在`on_touch_down`的handler创建一个新线，然后储存到`touch.ud`。现在我们要用直线来代替空心圈。\n",
    "- 在`on_touch_move`里面增加一个新点到线的末尾。我们增加的是直线元素，但是事件处理过程是每秒调用很多次实现这条线，每次都很短，最终看起来就很平滑。\n",
    "\n",
    ">更先进的图形程序可以用复杂的算法让线条呈现的更真实。包括贝塞尔曲线实现线条的高分辨率的无缝连接，并且从点的速度和压力推断线的厚度。这些具体的技术我们不打算实现了，不过读者可以作为一个练习。\n",
    "\n",
    "上述过程的代码如下："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {
    "collapsed": false
   },
   "outputs": [],
   "source": [
    "from kivy.graphics import Color, Line\n",
    "    class CanvasWidget(Widget):\n",
    "        def on_touch_down(self, touch):\n",
    "            if Widget.on_touch_down(self, touch):\n",
    "                return\n",
    "            with self.canvas:\n",
    "                Color(*get_color_from_hex('#0080FF80'))\n",
    "                touch.ud['current_line'] = Line(\n",
    "                    points=(touch.x, touch.y), width=2)\n",
    "        def on_touch_move(self, touch):\n",
    "            if 'current_line' in touch.ud:\n",
    "                touch.ud['current_line'].points += (\n",
    "                    touch.x, touch.y)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "这样就可以画线了。之后让我们来实现颜色选择功能，不断的完善我们的画图app。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "##调色板"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "画图app当然不能没有调色板。调色板其实就是可选颜色列表，可以让颜色选取很简单。通过图像编辑器都有调色板，带有全真彩24位色16,777,216种。如下图所示：\n",
    "![colorpalettewindow](kbpic/2.7colorpalettewindow.png)\n",
    "但是，就是你不打算完成一个主流的图像编辑器，我们也打算限制颜色的种类。因为对那些没有色彩常识的人来说，放一堆颜色只会让人头大。而且，互联网上的UI设计用色也会逐渐统一。\n",
    "\n",
    "在我们的app中，我们打算使用[扁平化的UI设计风格](http://designmodo.github.io/Flat-UI/)，基于一列精心挑选的颜色。当然，你可以选自己喜欢的颜色，因人而异。\n",
    "\n",
    ">颜色是一门学问，尤其是具体任务的兼容性与稳定性。低对比度的组合可能用来装饰元素或者标题，但是它们不符合正文的风格；另外，高对比度的颜色，如白与黑，不容易吸引注意力。\n",
    "\n",
    ">因此，颜色使用的首要原则是除非你很专业，否则用别人调好的颜色。最好的起点就是操作系统的用色。一些精彩案例如下：\n",
    "- [Tango调色板](http://tango.freedesktop.org/Tango_Icon_Theme_Guidelines)，在Linux开源环境中使用广泛。\n",
    "- Google在2014年GoogleIO大会上发布的[Material design](https://www.google.com/design/material-design.pdf)。\n",
    "- 非官方的[iOS 7颜色风格](http://ios7colors.com/)，超赞。\n",
    "\n",
    ">还有很多调色板可以学习，自行Google之。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "###按钮的子类"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "因为我使用的颜色很少，所以用单选按钮就可以了。Kivy的`ToggleButton`可以实现功能，不过有个限制：在一个单选组内，所有的按钮可以同时不选。也就是说，画图的时候可能没颜色。当然我们也可以设定默认颜色，但是用户可能会觉得很奇怪，所有我们不打算这么用。\n",
    "\n",
    "Python的OOP模式可以很好的解决这个问题，我们可以继承`ToggleButton`类，然后改造它的功能。之后，每次都会有一个颜色被选中了。\n",
    "\n",
    "子类还会实现另外一个功能：在调色板上，我们想让每个颜色按钮有唯一颜色。我们可以用之前的技术为每个按钮分配背景色，那就要一堆代码来分配。但是，我们如果写一个背景色属性，就可以在`paint.kv`文件里面分配了。\n",
    "\n",
    "这样就可以在`paint.kv`文件中使用按钮时保持调色板定义的可读性，同时在子类中实现的具体的细节——会展示OOP程序应该怎样实现。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "###去掉全不选功能"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "首先，让我们把全不选的功能去掉。\n",
    "\n",
    "首先，让我们实现一个标准的`ToggleButton`部件。我们之间在`paint.kv`文件里面增加如下代码：\n",
    "\n",
    "```yaml\n",
    "BoxLayout:\n",
    "    orientation: 'horizontal'\n",
    "    padding: 3\n",
    "    spacing: 3\n",
    "    x: 0\n",
    "    y: 0\n",
    "    width: root.width\n",
    "    height: 40\n",
    "    \n",
    "    ToggleButton:\n",
    "        group: 'color'\n",
    "        text: 'Red'\n",
    "    \n",
    "    ToggleButton:\n",
    "        group: 'color'\n",
    "        text: 'Blue'\n",
    "        state: 'down'\n",
    "```\n",
    "\n",
    "我们用了与`BoxLayout`类似的方式，每个颜色按钮单独分配一个工具栏。布局部件本文的位置是绝对的，其`x`和`y`的值都是0，也就是左下角，宽度与`CanvasWidget`一致。\n",
    "\n",
    "每个`ToggleButton`都属于同一`color`组。因此同一时间只有一个颜色可以被选中。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "####改写标准行为"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "要实现改写，让我们定义`ToggleButton`子类："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "from kivy.uix.behaviors import ToggleButtonBehavior\n",
    "from kivy.uix.togglebutton import ToggleButton\n",
    "\n",
    "class RadioButton(ToggleButton):\n",
    "    def _do_press(self):\n",
    "        if self.state == 'normal':\n",
    "            ToggleButtonBehavior._do_press(self)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "这样当按下按钮，状态`'normal'`就会变成`'down'`。\n",
    "\n",
    "现在我们把`paint.kv`文件里面`ToggleButton`改成`RadioButton`，立刻就会看到不同。\n",
    "\n",
    "这也是Kivy框架最吸引人的地方：小代码实现大功能。\n",
    ">要在Kivy语言中使用`RadioButton`，其定义需要在导入`main.py`文件。由于现在只有一个Python文件，这并不重要，但是一定记住：自定义的Kivy部件，和其他的Python类和函数一样，需要在使用之前被导入。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "###彩色按钮"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "现在按钮的功能正常了，我们把彩色按钮都做出来。如下图所示：\n",
    "![Paintappcolorpalette](kbpic/2.8Paintappcolorpalette.png)\n",
    "要实现这些，我们得用`background_color`属性。Kivy的背景色不仅可以使用单一颜色，可以用彩色；我们首先需要一个纯白色背景，然后画上想要的颜色。这样我们就只要为任意数量的彩色按钮准备两种模式（正常和按下的）即可。\n",
    "\n",
    "这和第一章时钟app是一样的。除了按钮的中心区域允许着色，选中的状态有个黑边。\n",
    "![colorbuttontexture](kbpic/2.9colorbuttontexture.png)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "####新按钮"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "加油！我们就快完工了，在`paint.kv`里面加入新类`ColorButton`：\n",
    "```yaml\n",
    "<ColorButton@RadioButton>:\n",
    "    group: 'color'\n",
    "    on_release: app.canvas_widget.set_color(self.background_color)\n",
    "    background_normal: 'color_button_normal.png'\n",
    "    background_down: 'color_button_down.png'\n",
    "    border: (3, 3, 3, 3)\n",
    "```\n",
    "你会发现，我们把`group: 'color'`移到这里避免重复代码。\n",
    "\n",
    "我们还要配置`on_release`事件handler，作用于已经被选中的按钮。现在，每个按钮已经把自己的`background_color`属性传递给事件handler，剩下的事情就是把颜色分配给画布。这个事件将由`CanvasWidget`处理，需要通过`PaintApp`类显示出来。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "class PaintApp(App):\n",
    "    def build(self):\n",
    "        # set_color()方法后面实现\n",
    "        self.canvas_widget = CanvasWidget()\n",
    "        self.canvas_widget.set_color(\n",
    "            get_color_from_hex('#2980B9'))\n",
    "        return self.canvas_widget"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "这么配置的原因是我们不能在`paint.kv`文件的类定义中使用`root`；因为那样会指向`ColorButton`自身（类规则里面的根定义在`paint.kv`文件的顶层）。我们还可以设置默认颜色，就像代码里显示的。\n",
    "\n",
    "在`main.py`文件里面，让我们来实现`CanvasWidget`的`set_color()`方法，可以当作是`ColorButton`的事件handler。代码很简单，就是把颜色作为参数："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "def set_color(self, new_color):\n",
    "    self.canvas.add(Color(*new_color))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "####定义调色板"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "下面我们来定义调色板。首先让我们把`RadioButton`从`paint.kv`文件中删掉。\n",
    "\n",
    "为了使用CSS颜色定义方式，我们需要将适当的函数导入`paint.kv`文件。把下面这行代码放在`paint.kv`文件开头。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "#:import C kivy.utils.get_color_from_hex"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "这行代码实际上和Python的代码一样："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "from kivy.utils import get_color_from_hex as C"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "我们使用扁平化设计的配色方式，代码如下：\n",
    "```yaml\n",
    "BoxLayout:\n",
    "    # ...\n",
    "    ColorButton:\n",
    "        background_color: C('#2980b9')\n",
    "        state: 'down'\n",
    "        \n",
    "    ColorButton:\n",
    "        background_color: C('#16A085')\n",
    "        \n",
    "    ColorButton:\n",
    "        background_color: C('#27AE60')\n",
    "```\n",
    "\n",
    "很简单吧，这样就为每个`ColorButton`按钮定义了`background_color`属性。其他的属性都是继承于Python中`ColorButton`类的定义。\n",
    "\n",
    "这样，增加任意数量的按钮都可以很好的排列了。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "##设置线的宽度"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "最后一个，也是最简单的功能就是设置线条的宽度。如下图所示，我们可以重用前面调色板的资源和样式。\n",
    "\n",
    "这个UI也是一种`RadioButton`子类，命名为`LineWidthButton`。在`paint.kv`文件中就是这样：\n",
    "\n",
    "```yaml\n",
    "<LineWidthButton@ColorButton>:\n",
    "    group: 'line_width'\n",
    "    on_release: app.canvas_widget.set_line_width(self.text)\n",
    "    color: C('#2C3E50')\n",
    "    background_color: C('#ECF0F1')\n",
    "```\n",
    "\n",
    "与`ColorButton`不同之处在于第2、3行代码。这些按钮属于另外一组，由其他的事件handler触发。当然，这两组按钮依然很相似。\n",
    "\n",
    "布局很简单，和调色板的样式一致，只是垂直摆放：\n",
    "```yaml\n",
    "BoxLayout:\n",
    "    orientation: 'vertical'\n",
    "    padding: 2\n",
    "    spacing: 2\n",
    "    x: 0\n",
    "    top: root.top\n",
    "    width: 80\n",
    "    height: 110\n",
    "    \n",
    "    LineWidthButton:\n",
    "        text: 'Thin'\n",
    "        \n",
    "    LineWidthButton:\n",
    "        text: 'Normal'\n",
    "        state: 'down'\n",
    "        \n",
    "    LineWidthButton:\n",
    "        text: 'Thick'\n",
    "```\n",
    ">注意`CanvasWidget.set_line_width`事件监听器会接受宽度调节按钮的`text`属性。这样实现是为了简化，允许我们为每一个按钮定义一个唯一的宽度值。\n",
    ">实际开发中，这种方法固然无可厚非。但是，当我们要把文字翻译成日语或法语的时候，这种对应关系就丢失了。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "###改变线条宽度"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "让我们把前面做好的模块都组合起来，这样就可以控制线条的粗细了。我们把线条宽度存储在`CanvasWidget.line_width`变量中，与按钮的文字一一对应，然后用`on_touch_down`触发事件改变线条宽度。代码如下："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "class CanvasWidget(Widget):\n",
    "    line_width = 2\n",
    "    def on_touch_down(self, touch):\n",
    "        # ...\n",
    "        with self.canvas:\n",
    "            touch.ud['current_line'] = Line(\n",
    "                points=(touch.x, touch.y),\n",
    "                width=self.line_width)\n",
    "            \n",
    "def set_line_width(self, line_width='Normal'):\n",
    "    self.line_width = {\n",
    "        'Thin': 1, 'Normal': 2, 'Thick': 4\n",
    "    }[line_width]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "这样就完成Kivy的画图app了，开始画图吧。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "##总结"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "这一章，我们重点学习了Kivy应用开发中的一些方法，包括自定义窗口，改变鼠标光标，窗口大小，背景色，通过画布指令绘制自定义的图形，正确的处理支持多平台的触摸事件，并且考虑多点触控的情况。\n",
    "\n",
    "在完成画图app之后，关于Kivy的一件显而易见的事情就是这个框架具有高度的开放性和通用性。不需要一大堆死板的组件，Kivy让开发者可以通过图形基本元素和行为的运用，让自定义模块变得简单灵活。也就是说，Kivy没有自带很多开箱即用的部件，但是通过几行Python代码就可以做出需要的东西。\n",
    "\n",
    "模块化的API设计方法缺乏美感，因为它限制了设计的柔性。最终的结果完全的满足你对项目的需求。客户总想要一些爆点，比如三角形按钮——当然，你还可以为它增加质地，这些都可以两三行代码搞定。（假如你想用**WinAPI**做一个三角形按钮。那就真掉坑里了。）\n",
    "\n",
    "Kivy的自定义部件还可以重用。实际上，你可以把`main.py`的`CanvasWidget`模块导入其他应用。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "##自然用户界面"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "我们的第二个应用比第一个应用更具交互性。不仅是在按钮上，还有多点触控手势。\n",
    "\n",
    "所有的窗口都支持触摸屏，对用户来说这是普遍共识，尤其在触摸屏设备上。只要用手指就可以绘画，好像在真实的画布上，即使手指很脏也可以上面画画。\n",
    "\n",
    "这种界面被称为NUI(自然界面，natural user interface)。有一些有趣的特性：NUI应用可以被小朋友或者宠物使用——可以在屏幕上看到和触摸图形元素。这是一种自然、直观的界面，一种“不需要思考”的事情，与[Norton Commander](http://en.wikipedia.org/wiki/Norton_Commander)的反直觉截然不同。直觉不应该接受蓝屏、ASCII码的表现形式。\n",
    "\n",
    "下一章，我们将建立另外一个Kivy程序，只能Android用。将Python与Android API的Java类很好的结合在一起。"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.5.1"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 0
}
